import { Buffer } from 'buffer';
import protocol from './protocol';
import { Storage } from './storage';
import * as helpers from './helpers';
import { Logger } from '@nestjs/common';


export default class Tracker {
	config: any;
	socket: any;
	_name: string;

	private readonly logger: Logger = new Logger(Tracker.name);

	constructor(config: any, socket: any) {
		this.config = config;
		this.socket = socket;
		this._name = `${config.host}:${config.port}`;
	}

	/**
	 * query storage server to upload file
	 * 获取指定group的storage实例，如果不指定则tracker server会随机返回1个
	 * @param group
	 */
	getStoreStorage(group?: string): Promise<Storage> {
		return new Promise((resolve, reject) => {
			// 验证group是否过长
			if (group && group.length > protocol.FDFS_GROUP_NAME_MAX_LEN) {
				reject(new Error('group name [' + group + '] is too long'));
				return;
			}
			this.logger.debug('get a upload storage server from tracker server: [%s]', this._name);

			const socket = this.socket;
			// 获取1个可用storage server 信息
			// 封装header并发送
			let command: number;
			let bodyLength: number;
			if (!group) {
				command = protocol.TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ONE;
				bodyLength = 0;
			} else {
				command = protocol.TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITH_GROUP_ONE;
				bodyLength = protocol.FDFS_GROUP_NAME_MAX_LEN;
			}
			const header = protocol.packHeader(command, bodyLength, 0);
			this.logger.debug('send header to tracker server [%s]', this._name);
			socket.write(header);

			// 发送body
			if (group) {
				const body = Buffer.alloc(bodyLength);
				// 默认都填充上0
				const groupBL = Buffer.byteLength(group, this.config.charset);
				body.write(group, 0, groupBL, this.config.charset);
				this.logger.debug('send body to tracker server [%s]', this._name);
				socket.write(body);
			}

			protocol.recvPacket(
				socket,
				protocol.TRACKER_PROTO_CMD_RESP,
				protocol.TRACKER_QUERY_STORAGE_STORE_BODY_LEN,
				(err: Error, body: Buffer) => {
					if (err) {
						reject(err);
						return;
					}
					const storageConfig: Record<string, any> = this._parseStorage(body, this.config.charset, true);
					storageConfig.timeout = this.config.timeout;
					storageConfig.charset = this.config.charset;
					const storage = new Storage(storageConfig);
					this.logger.debug('get store storage server info: %j ', storage.config);
					resolve(storage);
				}
			);
		});
	}

	/**
	 * query which storage server to download the file
	 * 若只传1个参数则认为是fileId
	 *
	 * * TRACKER_PROTO_CMD_SERVICE_QUERY_FETCH_ONE
		# function: query which storage server to download the file
		# request body:
		@ FDFS_GROUP_NAME_MAX_LEN bytes: group name
		@ filename bytes: filename

		# response body:
		@ FDFS_GROUP_NAME_MAX_LEN bytes: group name
		@ FDFS_IPADDR_SIZE - 1 bytes: storage server ip address
		@ FDFS_PROTO_PKG_LEN_SIZE bytes: storage server port
	 *
	 * @param fileId
	 */
	getFetchStorage(fileId: string): Promise<Storage> {
		this.logger.debug('get a fetch storage server from tracker server: [%s]', this._name);
		return this.getFetchOrUpdateStorage(protocol.TRACKER_PROTO_CMD_SERVICE_QUERY_FETCH_ONE, fileId);
	}

	getUpdateStorage(fileId: string): Promise<Storage> {
		this.logger.debug('get a update storage server from tracker server: [%s]', this._name);
		return this.getFetchOrUpdateStorage(protocol.TRACKER_PROTO_CMD_SERVICE_QUERY_UPDATE, fileId);
	}

	/**
	 * # request body:
	 *   @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
	 *   @ filename bytes: filename
	 *
	 * # response body:
	 *   @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
	 *   @ FDFS_IPADDR_SIZE - 1 bytes: storage server ip address
	 *   @ FDFS_PROTO_PKG_LEN_SIZE bytes: storage server port
	 * @param command TRACKER_PROTO_CMD_SERVICE_QUERY_FETCH_ONE or TRACKER_PROTO_CMD_SERVICE_QUERY_UPDATE
	 * @param fileId
	 * @private
	 */
	private getFetchOrUpdateStorage(command: number, fileId: string): Promise<Storage> {
		const gf = helpers.id2gf(fileId);
		const group = gf.group;
		const filename = gf.filename;
		this.logger.debug('group: %s, filename: %s', group, filename);

		const socket = this.socket;
		const packet = protocol.packFileId(command, group, filename, this.config.charset);
		socket.write(packet);

		return new Promise((resolve, reject) => {
			protocol.recvPacket(
				socket,
				protocol.TRACKER_PROTO_CMD_RESP,
				protocol.FDFS_GROUP_NAME_MAX_LEN + protocol.FDFS_IPADDR_SIZE - 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE,
				(err: Error, body: Buffer) => {
					if (err) {
						reject(err);
						return;
					}
					const storageConfig = this._parseStorage(body, this.config.charset);
					storageConfig.timeout = this.config.timeout;
					storageConfig.charset = this.config.charset;
					const storage = new Storage(storageConfig);
					this.logger.log('get storage server info: %j ', storage.config);
					resolve(storage);
				}
			);
		});
	}

	// helper methods
	/**
	 *   @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
	 *   @ FDFS_IPADDR_SIZE - 1 bytes: storage server ip address
	 *   @ FDFS_PROTO_PKG_LEN_SIZE bytes: storage server port
	 *   @1 byte: store path index on the storage server {可以没有这个字节}
	 *
	 * @param {Buffer} body
	 * @param {BufferEncoding} charset
	 * @param {Boolean} hasPathIndex
	 * @private {Object}
	 */
	_parseStorage(body: Buffer, charset: BufferEncoding, hasPathIndex?: boolean): Record<string, any> {
		const result: Record<string, any> = {};

		const group = helpers.trim(body.toString(charset, 0, protocol.FDFS_GROUP_NAME_MAX_LEN));
		const ip = helpers.trim(body.toString(charset, protocol.FDFS_GROUP_NAME_MAX_LEN, protocol.FDFS_GROUP_NAME_MAX_LEN + protocol.FDFS_IPADDR_SIZE - 1));
		const port = Number('0x' + body.toString('hex',
			protocol.FDFS_GROUP_NAME_MAX_LEN + protocol.FDFS_IPADDR_SIZE - 1,
			protocol.FDFS_GROUP_NAME_MAX_LEN + protocol.FDFS_IPADDR_SIZE - 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE));

		result.group = group;
		result.host = ip;
		result.port = port;

		if (hasPathIndex &&
			body.length > protocol.FDFS_GROUP_NAME_MAX_LEN + protocol.FDFS_IPADDR_SIZE - 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE) {
			const storePathIndex = Number('0x' + body.toString('hex',
				protocol.FDFS_GROUP_NAME_MAX_LEN + protocol.FDFS_IPADDR_SIZE - 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE,
				protocol.FDFS_GROUP_NAME_MAX_LEN + protocol.FDFS_IPADDR_SIZE - 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE + 1));

			result.storePathIndex = storePathIndex;
		}

		return result;
	}

	listGroups(): Promise<any> {
		const socket = this.socket;

		// 封装header并发送
		const command = protocol.TRACKER_PROTO_CMD_SERVER_LIST_GROUP;
		const header = protocol.packHeader(command);
		this.logger.debug('send header to tracker server [%s]', this._name);

		socket.write(header);

		return new Promise((resolve, reject) => {
			protocol.recvPacket(
				socket,
				protocol.TRACKER_PROTO_CMD_RESP,
				null,
				(err: Error, body: Buffer) => {
					if (err) {
						reject(err);
						return;
					}
					if (body.length % 105 !== 0) {
						reject(new Error('byte array length:' + body.length + ' is invalid!'));
						return;
					}
					const res = {
						count: body.length / 105,
						groups: []
					};
					const charset = this.config.charset;
					let offset = 0;
					for (let i = 0; i < res.count; i++) {
						offset = 105 * i;
						res.groups.push({
							groupName: helpers.trim(body.toString(charset, offset, offset + protocol.FDFS_GROUP_NAME_MAX_LEN)),
							totalMB: helpers.buffer2Number(body, protocol.FDFS_GROUP_NAME_MAX_LEN + 1),
							freeMB: helpers.buffer2Number(body, protocol.FDFS_GROUP_NAME_MAX_LEN + 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE * 1),
							trunkFreeMB: helpers.buffer2Number(body, protocol.FDFS_GROUP_NAME_MAX_LEN + 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE * 2),
							storageCount: helpers.buffer2Number(body, protocol.FDFS_GROUP_NAME_MAX_LEN + 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE * 3),
							storagePort: helpers.buffer2Number(body, protocol.FDFS_GROUP_NAME_MAX_LEN + 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE * 4),
							storageHttpPort: helpers.buffer2Number(body, protocol.FDFS_GROUP_NAME_MAX_LEN + 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE * 5),
							activeCount: helpers.buffer2Number(body, protocol.FDFS_GROUP_NAME_MAX_LEN + 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE * 6),
							currentWriteServer: helpers.buffer2Number(body, protocol.FDFS_GROUP_NAME_MAX_LEN + 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE * 7),
							storePathCount: helpers.buffer2Number(body, protocol.FDFS_GROUP_NAME_MAX_LEN + 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE * 8),
							subdirCountPerPath: helpers.buffer2Number(body, protocol.FDFS_GROUP_NAME_MAX_LEN + 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE * 9),
							currentTrunkFileId: helpers.buffer2Number(body, protocol.FDFS_GROUP_NAME_MAX_LEN + 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE * 10),
						});
					}
					resolve(res);
				}
			);
		});
	}

	listStorages(group: string): Promise<any> {
		const socket = this.socket;

		// 封装header并发送
		const command = protocol.TRACKER_PROTO_CMD_SERVER_LIST_STORAGE;
		const bodyLength = protocol.FDFS_GROUP_NAME_MAX_LEN;
		const header = protocol.packHeader(command, bodyLength);

		this.logger.debug('send header to tracker server [%s]', this._name);
		socket.write(header);

		const body = Buffer.alloc(bodyLength);
		// 默认都填充上0
		body.fill(0);
		const groupBL = Buffer.byteLength(group, this.config.charset);
		body.write(group, 0, groupBL, this.config.charset);
		this.logger.debug('send body to tracker server [%s]', this._name);
		socket.write(body);

		return new Promise((resolve, reject) => {
			protocol.recvPacket(
				socket,
				protocol.TRACKER_PROTO_CMD_RESP,
				null,
				(err: Error, body: Buffer) => {
					if (err) {
						reject(err);
						return;
					}
					let baseOffset = 0;
					if (body.length % 600 === 0) {
						baseOffset = 600;
					} else if (body.length % 612 === 0) {
						baseOffset = 612;
					} else {
						reject(new Error('byte array length:' + body.length + ' is invalid!'));
						return;
					}
					const res = {
						count: body.length / baseOffset,
						storages: []
					};
					const charset = this.config.charset;
					let offset = 0;
					for (let i = 0; i < res.count; i++) {
						offset = baseOffset * i;
						const s: Record<string, any> = {};

						s.status = body[offset];
						offset += 1;
						s.id = helpers.trim(body.toString(charset, offset, protocol.FDFS_STORAGE_ID_MAX_SIZE));
						offset += protocol.FDFS_STORAGE_ID_MAX_SIZE;
						s.ipAddr = helpers.trim(body.toString(charset, offset, offset + protocol.FDFS_IPADDR_SIZE));
						offset += protocol.FDFS_IPADDR_SIZE;
						s.domainName = helpers.trim(body.toString(charset, offset, offset + protocol.FDFS_DOMAIN_NAME_MAX_SIZE));
						offset += protocol.FDFS_DOMAIN_NAME_MAX_SIZE;
						s.srcIpAddr = helpers.trim(body.toString(charset, offset, offset + protocol.FDFS_IPADDR_SIZE));
						offset += protocol.FDFS_IPADDR_SIZE;
						s.version = helpers.trim(body.toString(charset, offset, offset + protocol.FDFS_VERSION_SIZE));
						offset += protocol.FDFS_VERSION_SIZE;
						s.joinTime = new Date(helpers.buffer2Number(body, offset) * 1000);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.upTime = new Date(helpers.buffer2Number(body, offset) * 1000);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalMB = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.freeMB = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.uploadPriority = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.storePathCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.subdirCountPerPath = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.currentWritePath = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.storagePort = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.storageHttpPort = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						if (baseOffset === 612) {
							s.connectionAllocCount = helpers.buffer2ShortNumber(body, offset);
							offset += 4;
							s.connectionCurrentCount = helpers.buffer2ShortNumber(body, offset);
							offset += 4;
							s.connectionMaxCount = helpers.buffer2ShortNumber(body, offset);
							offset += 4;
						}
						s.totalUploadCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successUploadCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalAppendCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successAppendCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalModifyCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successModifyCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalTruncateCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successTruncateCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalSetMetaCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successSetMetaCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalDeleteCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successDeleteCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalDownloadCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successDownloadCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalGetMetaCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successGetMetaCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalCreateLinkCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successCreateLinkCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalDeleteLinkCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successDeleteLinkCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalUploadBytes = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successUploadBytes = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalAppendBytes = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successAppendBytes = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalModifyBytes = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successModifyBytes = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalDownloadloadBytes = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successDownloadloadBytes = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalSyncInBytes = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successSyncInBytes = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalSyncOutBytes = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successSyncOutBytes = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalFileOpenCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successFileOpenCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalFileReadCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successFileReadCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.totalFileWriteCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.successFileWriteCount = helpers.buffer2Number(body, offset);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.lastSourceUpdate = new Date(helpers.buffer2Number(body, offset) * 1000);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.lastSyncUpdate = new Date(helpers.buffer2Number(body, offset) * 1000);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.lastSyncedTimestamp = new Date(helpers.buffer2Number(body, offset) * 1000);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.lastHeartBeatTime = new Date(helpers.buffer2Number(body, offset) * 1000);
						offset += protocol.FDFS_PROTO_PKG_LEN_SIZE;
						s.ifTrunkServer = body[offset] !== 0;

						res.storages.push(s);
					}
					resolve(res);
				}
			);
		});
	}

}
